/*
 * Copyright © 2021-2022 Apple Inc. and the ServiceTalk project authors
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package io.servicetalk.grpc.api;

import io.servicetalk.concurrent.api.Single;
import io.servicetalk.http.api.HttpExecutionStrategies;
import io.servicetalk.http.api.HttpExecutionStrategy;
import io.servicetalk.http.api.HttpServiceContext;
import io.servicetalk.http.api.StreamingHttpRequest;
import io.servicetalk.http.api.StreamingHttpResponse;
import io.servicetalk.http.api.StreamingHttpResponseFactory;
import io.servicetalk.http.api.StreamingHttpService;
import io.servicetalk.http.api.StreamingHttpServiceFilter;
import io.servicetalk.http.api.StreamingHttpServiceFilterFactory;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import static io.servicetalk.concurrent.api.Single.succeeded;
import static io.servicetalk.grpc.api.GrpcHeaderNames.GRPC_STATUS;
import static io.servicetalk.grpc.api.GrpcHeaderValues.APPLICATION_GRPC;
import static io.servicetalk.grpc.api.GrpcStatusCode.fromCodeValue;
import static io.servicetalk.grpc.api.GrpcStatusException.serverCatchAllShouldLog;
import static io.servicetalk.grpc.api.GrpcUtils.newErrorResponse;

/**
 * Filter that maps known {@link Throwable} subtypes into an gRPC response with an appropriate {@link GrpcStatusCode}.
 * <p>
 * This filter is recommended to be placed as early as possible to make sure it captures all exceptions that may be
 * generated by other filters.
 */
public final class GrpcExceptionMapperServiceFilter implements StreamingHttpServiceFilterFactory {

    private static final Logger LOGGER = LoggerFactory.getLogger(GrpcExceptionMapperServiceFilter.class);

    /**
     * Instance of {@link GrpcExceptionMapperServiceFilter}.
     */
    public static final StreamingHttpServiceFilterFactory INSTANCE = new GrpcExceptionMapperServiceFilter();

    private GrpcExceptionMapperServiceFilter() {
        // Singleton
    }

    @Override
    public StreamingHttpServiceFilter create(final StreamingHttpService service) {
        return new StreamingHttpServiceFilter(service) {

            @Override
            public Single<StreamingHttpResponse> handle(final HttpServiceContext ctx,
                                                        final StreamingHttpRequest request,
                                                        final StreamingHttpResponseFactory responseFactory) {
                final Single<StreamingHttpResponse> handle;
                try {
                    handle = delegate().handle(ctx, request, responseFactory);
                } catch (Throwable cause) {
                    return succeeded(convertToGrpcErrorResponse("handle(...)", ctx, request, responseFactory,
                            cause));
                }
                return handle.onErrorReturn(cause ->
                        convertToGrpcErrorResponse("response", ctx, request, responseFactory, cause));
            }
        };
    }

    private static StreamingHttpResponse convertToGrpcErrorResponse(final String what,
                                                                    final HttpServiceContext ctx,
                                                                    final StreamingHttpRequest request,
                                                                    final StreamingHttpResponseFactory responseFactory,
                                                                    final Throwable cause) {
        final StreamingHttpResponse response = newErrorResponse(responseFactory, APPLICATION_GRPC, cause,
                ctx.executionContext().bufferAllocator(), null);
        if (serverCatchAllShouldLog(cause)) {
            log(response, false, what, ctx, request, cause);
        } else if (LOGGER.isDebugEnabled()) {
            log(response, true, what, ctx, request, cause);
        }
        return response;
    }

    private static void log(final StreamingHttpResponse response, final boolean debug, final String what,
                            final HttpServiceContext ctx, final StreamingHttpRequest request, final Throwable cause) {
        final CharSequence codeValue = response.headers().get(GRPC_STATUS);
        assert codeValue != null;
        final String format =
                "{} during a {} processing for connection='{}', request='{} {} {}' was mapped to grpc-status: {} ({})";
        if (debug) {
            LOGGER.debug(format, "Exception", what, ctx, request.method(), request.requestTarget(),
                    request.version(), codeValue, fromCodeValue(codeValue), cause);
        } else {
            LOGGER.error(format, "Unexpected exception", what, ctx, request.method(), request.requestTarget(),
                    request.version(), codeValue, fromCodeValue(codeValue), cause);
        }
    }

    @Override
    public HttpExecutionStrategy requiredOffloads() {
        return HttpExecutionStrategies.offloadNone();
    }
}
